package cs340.stormtroopers.hypeerweb;

import gui.GUIFacade;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Random;



// TODO 100% coverage in cobertura report
/**
 * <pre>
 * Used to send a message from a source node to a target node. The actual message is the targetOperation to be 
 * performed on the target Node. The "targetOperation" is abstract and is to be overridden in a visitor that 
 * does the actual work. There is also an intermediateOperation that may be performed on nodes visited on the 
 * way to the target node. 
 * 
 * 	<b>Domain:</b>	
 * 		None
 * </pre>
 * 
 * @author William Highland
 */
public abstract class SendVisitor extends Visitor
{
	/**
	 * The key used in a key-value pair of the parameters to identify the target webId. 
	 */
	protected static String TARGET_KEY = "target"; 
	
	private ArrayList<Integer> alreadyVisitedNodes;
	
	/**
	 * The default constructor for SendVisitor
	 * 
	 * @pre None
	 * @post True
	 */	
	public SendVisitor()
	{
		alreadyVisitedNodes = new ArrayList<Integer>();
	}
	
	
	/**
	 * The SendVisitor visitor expects the parameters to contain a target. This method initializes the 
	 * parameters with the key-pair (TARGET_KEY, target). If more parameters are required in a subclass
	 * this method is overridden. Normally this method is only called by the source node before sending 
	 * the "message". 
	 * 
	 * @param target - The WebId of the Node we are to perform the target operation on. 
	 * 
	 * @pre There exists a Node n such that n is a member of the HyPeerWeb and n.WebId is equal to target
	 * @post Parameters contains the mapping (TARGET_KEY, target) 
	 * 
	 */
	public static Parameters createInitialParameters(int target)
	{
		Parameters parameters = new Parameters();
		parameters.set(TARGET_KEY, target);
		return parameters;
	}
	
	/**
	 * Visits the given node in order to send a message to a node. The target node's WebId 
	 * (and the message to send it) is specified in parameters. 
	 * 
	 * @param node The node that is being visited. 
	 * @param parameters The parameters that specify the target Node's WebId and the message to send it. 
	 * It MUST contain these mappings.
	 * 
	 * @pre Node must be a valid node and be contained in the HyPeerWeb. Parameters must contain 
	 * 	a mapping from "target" to the target node. The WebId specified by target must correspond to the
	 *  WebId of a Node in the HyPeerWeb
	 * @post The target node will have been visited. 
	 */
	@Override
	public void visit(Node node, Parameters parameters)
	{
		HyPeerWeb.getSingleton().printToAllTracePanels("SEND  visiting "+node.getWebIdValue());
		//Error-checking
		if(!parameters.containsKey(TARGET_KEY))
		{
			System.err.println("You must specify a target in the parameters object " +
					"before trying to send. \n   Otherwise I don't know who I'm sending to.\n" +
					"The mapping should look like " + TARGET_KEY + ":THE_TARGET. Where THE_TARGET " +
					"is the WebId of the target Node ");
			return;
		}
		
		alreadyVisitedNodes.add(node.getWebIdValue());		
		if(node.getWebIdValue() == (Integer) parameters.get(TARGET_KEY))
		{
			targetOperation(node, parameters);
		}
		else
		{
			Node closest = findClosestToTarget(node, (Integer) parameters.get(TARGET_KEY));
			intermediateOperation(closest, parameters);
			closest.accept(this, parameters);
		}		
	}
	
	/**
	 * The abstract operation to be performed on the targetNode. Must be overridden in any concrete subclass. 
	 * 
	 * 
	 * @param node - The target Node that the operation will be performed on.
	 * @param parameters - The parameters to be passed to the target operation. 
	 * 
	 * @pre node is not null AND parameters is not null
	 * @post True 
	 * 
	 */
	protected abstract void targetOperation(Node node, Parameters parameters);

	/**
	 * The intermediate operation to be performed on a node as we traverse to the target node.
	 * Often this method does nothing. 
	 * 
	 * @param node - The intermediate Node
	 * @param parameters- The parameters to be used when executing the intermediate operation.
	 * 
	 * @pre node is not null AND parameters is not null
	 * @post True 
	 * 
	 */
	protected abstract void intermediateOperation(Node node, Parameters parameters);

	
	private Node findClosestToTarget(Node startNode, int target)
	{
		//List of nodes that are closest to the target
		//Needed to implement the random access when multiple nodes would be optimal
		ArrayList<Node> closestNodesToTarget = new ArrayList<Node>();
		
		//Used to determine how close the node is to target
		Connections connections = startNode.getConnections();
		assert(connections != null);
		
		//In reality a node should never differ more than 32 bits from another.
		//And that would be in a HyPeerWeb of over 8 billion nodes. 
		//We make it huge just to be safe
		int shortestDistanceSoFar = 1029;
		for(Node connectorNode : connections)
		{
			if(connectorNode.getWebIdValue() == target)
			{
				return connectorNode;
			}
			
			//Be sure not to cyclically re-visit nodes
			else if(!alreadyVisitedNodes.contains(connectorNode.getWebIdValue()))
			{
				int connectorDistance = numberOfBitsThatDiffer(target, connectorNode.getWebIdValue());
				if(connectorDistance < shortestDistanceSoFar)
				{
					//This Node is closer than any other so far
					shortestDistanceSoFar = connectorDistance; 
					closestNodesToTarget.clear();
					closestNodesToTarget.add(connectorNode);
				}
				else if(connectorDistance == shortestDistanceSoFar && 
						(startNode.getWebIdValue() != connectorNode.getWebIdValue()))
				{
					//Otherwise this is another Node that is equally close to the target
					closestNodesToTarget.add(connectorNode);
				}
			}
		}
		
		//Randomly choose a node that is equally close to the target
		int index = 0;
		if(closestNodesToTarget.size() > 1)
		{
			Random random = new Random();
			index = random.nextInt(closestNodesToTarget.size());
		}
		return closestNodesToTarget.get(index);
	}
	
	/**
	 * For debugging purposes, this returns the path taken by this SendVisitor
	 * 
	 * @pre This visitor must have already visited a Node 
	 * @post Result will contain a String representation of the WebId's of each Node that was visited by this SendVisitor
	 */
	public String getStringPath()
	{
		StringBuilder result = new StringBuilder();
		
		for(Integer i : alreadyVisitedNodes)
		{
			result.append("I'm visiting Node [" + i + "] \n"); 
		}
		
		return result.toString();
	}
	
	/**
	 * Returns the number of bits that differ between the two integers. 
	 * 
	 * @pre None
	 * @post Result will be equal to the number of bits that differ between the integers start and end.
	 *
	 */
	private int numberOfBitsThatDiffer(int start, int end)
	{
		int result = Integer.bitCount(start ^ end);
		return result;
	}
	
	/**
	 * Returns the path taken by the SendVisitor, including the target Node and starting node
	 * 
	 * 
	 * @return - The path taken by the Visitor
	 */
	public ArrayList<Integer> getPath()
	{
		return alreadyVisitedNodes;
	}

}
